/**
 * MojoSetup; a portable, flexible installation application.
 *
 * Please see the file LICENSE.txt in the source's root directory.
 *
 *  This file written by Ryan C. Gordon.

      Copyright (c) 2006-2010 Ryan C. Gordon and others.

   This software is provided 'as-is', without any express or implied warranty.
   In no event will the authors be held liable for any damages arising from
   the use of this software.

   Permission is granted to anyone to use this software for any purpose,
   including commercial applications, and to alter it and redistribute it
   freely, subject to the following restrictions:

   1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software in a
   product, an acknowledgment in the product documentation would be
   appreciated but is not required.

   2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

   3. This notice may not be removed or altered from any source distribution.

       Ryan C. Gordon <icculus@icculus.org>

 */

#include "fileio.h"

#if !SUPPORT_ZIP
MojoArchive *MojoArchive_createZIP(MojoInput *io) { return NULL; }
#else

#include <time.h>
#include <errno.h>

#include "miniz.h"

/*
 * ZIP support routines, adapted from PhysicsFS (http://icculus.org/physfs/)
 */

#if __MOJOSETUP__
// Glue from PhysicsFS to MojoSetup follows...
typedef int8 PHYSFS_sint8;
typedef uint8 PHYSFS_uint8;
typedef int16 PHYSFS_sint16;
typedef uint16 PHYSFS_uint16;
typedef int32 PHYSFS_sint32;
typedef uint32 PHYSFS_uint32;
typedef int64 PHYSFS_sint64;
typedef uint64 PHYSFS_uint64;

#define ERRPASS NULL
#define BAIL_IF_MACRO(cond, err, ret) do { if (cond) return ret; } while (0)
#define BAIL_MACRO(err, ret) return ret
#define GOTO_IF_MACRO(cond, err, jmp) do { if (cond) goto jmp; } while (0)

static void *mallocWrap(PHYSFS_uint64 s) { return xmalloc((uint32) s); }
static void *reallocWrap(void *p,PHYSFS_uint64 s){return xrealloc(p,(uint32)s);}
static void freeWrap(void *p) { free(p); }
typedef struct
{
    int (*Init)(void);   /**< Initialize. Can be NULL. Zero on failure. */
    void (*Deinit)(void);  /**< Deinitialize your allocator. Can be NULL. */
    void *(*Malloc)(PHYSFS_uint64);  /**< Allocate like malloc(). */
    void *(*Realloc)(void *, PHYSFS_uint64); /**< Reallocate like realloc(). */
    void (*Free)(void *); /**< Free memory from Malloc or Realloc. */
} PHYSFS_Allocator;

static PHYSFS_Allocator allocator = { 0, 0, mallocWrap, reallocWrap, freeWrap };

#define ERR_ZLIB_NEED_DICT     _("need dictionary")
#define ERR_ZLIB_DATA_ERROR    _("data error")
#define ERR_ZLIB_MEMORY_ERROR  _("memory error")
#define ERR_ZLIB_BUFFER_ERROR  _("buffer error")
#define ERR_ZLIB_VERSION_ERROR _("version error")
#define ERR_ZLIB_UNKNOWN_ERROR _("unknown error")

#define __PHYSFS_setError(x)

#define fvoid void
#define dvoid void

#define readui16(io, ptr) MojoInput_readui16((MojoInput *) (io), ptr)
#define readui32(io, ptr) MojoInput_readui32((MojoInput *) (io), ptr)
#define readui64(io, ptr) MojoInput_readui64((MojoInput *) (io), ptr)

static PHYSFS_sint64 __PHYSFS_platformRead(void *opaque, void *buffer,
                                    PHYSFS_uint32 size, PHYSFS_uint32 count)
{
    MojoInput *io = (MojoInput *) opaque;
    int64 rc = io->read(io, buffer, size * count);
    return rc / size;  // !!! FIXME: what if rc == -1?
}

static int __PHYSFS_readAll(void *opaque, void *buf, const PHYSFS_sint64 len)
{
    return (__PHYSFS_platformRead(opaque, buf, 1, (PHYSFS_uint32) len) == len);
}

static int __PHYSFS_platformSeek(void *opaque, PHYSFS_uint64 pos)
{
    MojoInput *io = (MojoInput *) opaque;
    return io->seek(io, pos) ? 1 : 0;
}

static int __PHYSFS_platformClose(void *opaque)
{
    MojoInput *io = (MojoInput *) opaque;
    io->close(io);
    return 1;
}

static PHYSFS_sint64 __PHYSFS_platformFileLength(void *opaque)
{
    MojoInput *io = (MojoInput *) opaque;
    return io->length(io);
}

static PHYSFS_sint64 __PHYSFS_platformTell(void *opaque)
{
    MojoInput *io = (MojoInput *) opaque;
    return io->tell(io);
}

#define PHYSFS_QUICKSORT_THRESHOLD 4
static void __PHYSFS_bubble_sort(void *a, size_t lo, size_t hi,
                         int (*cmpfn)(void *, size_t, size_t),
                         void (*swapfn)(void *, size_t, size_t))
{
    size_t i;
    int sorted;

    do
    {
        sorted = 1;
        for (i = lo; i < hi; i++)
        {
            if (cmpfn(a, i, i + 1) > 0)
            {
                swapfn(a, i, i + 1);
                sorted = 0;
            } /* if */
        } /* for */
    } while (!sorted);
} /* __PHYSFS_bubble_sort */


static void __PHYSFS_quick_sort(void *a, size_t lo, size_t hi,
                         int (*cmpfn)(void *, size_t, size_t),
                         void (*swapfn)(void *, size_t, size_t))
{
    size_t i;
    size_t j;
    size_t v;

    if ((hi - lo) <= PHYSFS_QUICKSORT_THRESHOLD)
        __PHYSFS_bubble_sort(a, lo, hi, cmpfn, swapfn);
    else
    {
        i = (hi + lo) / 2;

        if (cmpfn(a, lo, i) > 0) swapfn(a, lo, i);
        if (cmpfn(a, lo, hi) > 0) swapfn(a, lo, hi);
        if (cmpfn(a, i, hi) > 0) swapfn(a, i, hi);

        j = hi - 1;
        swapfn(a, i, j);
        i = lo;
        v = j;
        while (1)
        {
            while(cmpfn(a, ++i, v) < 0) { /* do nothing */ }
            while(cmpfn(a, --j, v) > 0) { /* do nothing */ }
            if (j < i)
                break;
            swapfn(a, i, j);
        } /* while */
        swapfn(a, i, hi-1);
        __PHYSFS_quick_sort(a, lo, j, cmpfn, swapfn);
        __PHYSFS_quick_sort(a, i+1, hi, cmpfn, swapfn);
    } /* else */
} /* __PHYSFS_quick_sort */


void __PHYSFS_sort(void *entries, size_t max,
                   int (*cmpfn)(void *, size_t, size_t),
                   void (*swapfn)(void *, size_t, size_t))
{
    /*
     * Quicksort w/ Bubblesort fallback algorithm inspired by code from here:
     *   http://www.cs.ubc.ca/spider/harrison/Java/sorting-demo.html
     */
    if (max > 0)
        __PHYSFS_quick_sort(entries, 0, max - 1, cmpfn, swapfn);
} /* __PHYSFS_sort */


typedef void (*PHYSFS_EnumFilesCallback)(void *, const char *, const char *);
#endif


/*
 * A buffer of ZIP_READBUFSIZE is allocated for each compressed file opened,
 *  and is freed when you close the file; compressed data is read into
 *  this buffer, and then is decompressed into the buffer passed to
 *  PHYSFS_read().
 *
 * Uncompressed entries in a zipfile do not allocate this buffer; they just
 *  read data directly into the buffer passed to PHYSFS_read().
 *
 * Depending on your speed and memory requirements, you should tweak this
 *  value.
 */
#if __MOJOSETUP__
#define ZIP_READBUFSIZE   (128 * 1024)
#else
#define ZIP_READBUFSIZE   (16 * 1024)
#endif

/*
 * Entries are "unresolved" until they are first opened. At that time,
 *  local file headers parsed/validated, data offsets will be updated to look
 *  at the actual file data instead of the header, and symlinks will be
 *  followed and optimized. This means that we don't seek and read around the
 *  archive until forced to do so, and after the first time, we had to do
 *  less reading and parsing, which is very CD-ROM friendly.
 */
typedef enum
{
    ZIP_UNRESOLVED_FILE,
    ZIP_UNRESOLVED_SYMLINK,
    ZIP_RESOLVING,
    ZIP_RESOLVED,
    ZIP_BROKEN_FILE,
    ZIP_BROKEN_SYMLINK
} ZipResolveType;


/*
 * One ZIPentry is kept for each file in an open ZIP archive.
 */
typedef struct _ZIPentry
{
    char *name;                         /* Name of file in archive        */
    struct _ZIPentry *symlink;          /* NULL or file we symlink to     */
    #if __MOJOSETUP__
    PHYSFS_uint16 perms;
    char *linkdest;
    #endif
    ZipResolveType resolved;            /* Have we resolved file/symlink? */
    PHYSFS_uint64 offset;               /* offset of data in archive      */
    PHYSFS_uint16 version;              /* version made by                */
    PHYSFS_uint16 version_needed;       /* version needed to extract      */
    PHYSFS_uint16 compression_method;   /* compression method             */
    PHYSFS_uint32 crc;                  /* crc-32                         */
    PHYSFS_uint64 compressed_size;      /* compressed size                */
    PHYSFS_uint64 uncompressed_size;    /* uncompressed size              */
    PHYSFS_sint64 last_mod_time;        /* last file mod time             */
} ZIPentry;

/*
 * One ZIPinfo is kept for each open ZIP archive.
 */
typedef struct
{
    char *archiveName;        /* path to ZIP in platform-dependent notation. */
    #if __MOJOSETUP__
    void *io;  /* a MojoInput pointer */
    int32 enumIndex;  /* index of last entry enumerated. */
    int64 offset;  /* byte offset from start of MojoInput where zip starts. */
    #endif
    int zip64;                /* non-zero if this is a Zip64 archive. */
    PHYSFS_uint64 entryCount; /* Number of files in ZIP.              */
    ZIPentry *entries;        /* info on all files in ZIP.            */
} ZIPinfo;

/*
 * One ZIPfileinfo is kept for each open file in a ZIP archive.
 */
typedef struct
{
    #if __MOJOSETUP__
    ZIPinfo *archive; /* archive this belongs to, for duplication. */
    #endif
    ZIPentry *entry;                      /* Info on file.              */
    void *handle;                         /* physical file handle.      */
    PHYSFS_uint32 compressed_position;    /* offset in compressed data. */
    PHYSFS_uint32 uncompressed_position;  /* tell() position.           */
    PHYSFS_uint8 *buffer;                 /* decompression buffer.      */
    z_stream stream;                      /* zlib stream state.         */
} ZIPfileinfo;


/* Magic numbers... */
#define ZIP_LOCAL_FILE_SIG                          0x04034b50
#define ZIP_CENTRAL_DIR_SIG                         0x02014b50
#define ZIP_END_OF_CENTRAL_DIR_SIG                  0x06054b50
#define ZIP64_END_OF_CENTRAL_DIR_SIG                0x06064b50
#define ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG  0x07064b50
#define ZIP64_EXTENDED_INFO_EXTRA_FIELD_SIG         0x0001

/* compression methods... */
#define COMPMETH_NONE 0
/* ...and others... */


#define UNIX_FILETYPE_MASK    0170000
#define UNIX_FILETYPE_SYMLINK 0120000

/*
 * Bridge physfs allocation functions to zlib's format...
 */
static voidpf zlibPhysfsAlloc(voidpf opaque, uInt items, uInt size)
{
    return(((PHYSFS_Allocator *) opaque)->Malloc(items * size));
} /* zlibPhysfsAlloc */

/*
 * Bridge physfs allocation functions to zlib's format...
 */
static void zlibPhysfsFree(voidpf opaque, voidpf address)
{
    ((PHYSFS_Allocator *) opaque)->Free(address);
} /* zlibPhysfsFree */


/*
 * Construct a new z_stream to a sane state.
 */
static void initializeZStream(z_stream *pstr)
{
    memset(pstr, '\0', sizeof (z_stream));
    pstr->zalloc = zlibPhysfsAlloc;
    pstr->zfree = zlibPhysfsFree;
    pstr->opaque = &allocator;
} /* initializeZStream */


static const char *zlib_error_string(int rc)
{
    switch (rc)
    {
        case Z_OK: return(NULL);  /* not an error. */
        case Z_STREAM_END: return(NULL); /* not an error. */
#ifndef _WIN32_WCE
        case Z_ERRNO: return(strerror(errno));
#endif
        case Z_NEED_DICT: return(ERR_ZLIB_NEED_DICT);
        case Z_DATA_ERROR: return(ERR_ZLIB_DATA_ERROR);
        case Z_MEM_ERROR: return(ERR_ZLIB_MEMORY_ERROR);
        case Z_BUF_ERROR: return(ERR_ZLIB_BUFFER_ERROR);
        case Z_VERSION_ERROR: return(ERR_ZLIB_VERSION_ERROR);
        default: return(ERR_ZLIB_UNKNOWN_ERROR);
    } /* switch */
} /* zlib_error_string */


/*
 * Wrap all zlib calls in this, so the physfs error state is set appropriately.
 */
static int zlib_err(int rc)
{
    const char *str = zlib_error_string(rc);
    if (str != NULL)
        __PHYSFS_setError(str);
    return(rc);
} /* zlib_err */


#if !__MOJOSETUP__  /* we have our own readuiXX() functions. */
/*
 * Read an unsigned 64-bit int and swap to native byte order.
 */
static int readui64(void *io, PHYSFS_uint64 *val)
{
    PHYSFS_uint64 v;
    BAIL_IF_MACRO(!__PHYSFS_readAll(io, &v, sizeof (v)), ERRPASS, 0);
    *val = PHYSFS_swapULE64(v);
    return 1;
} /* readui64 */

/*
 * Read an unsigned 32-bit int and swap to native byte order.
 */
static int readui32(void *in, PHYSFS_uint32 *val)
{
    PHYSFS_uint32 v;
    BAIL_IF_MACRO(__PHYSFS_platformRead(in, &v, sizeof (v), 1) != 1, NULL, 0);
    *val = PHYSFS_swapULE32(v);
    return(1);
} /* readui32 */


/*
 * Read an unsigned 16-bit int and swap to native byte order.
 */
static int readui16(void *in, PHYSFS_uint16 *val)
{
    PHYSFS_uint16 v;
    BAIL_IF_MACRO(__PHYSFS_platformRead(in, &v, sizeof (v), 1) != 1, NULL, 0);
    *val = PHYSFS_swapULE16(v);
    return(1);
} /* readui16 */
#endif


static PHYSFS_sint64 ZIP_read(fvoid *opaque, void *buf,
                              PHYSFS_uint32 objSize, PHYSFS_uint32 objCount)
{
    ZIPfileinfo *finfo = (ZIPfileinfo *) opaque;
    ZIPentry *entry = finfo->entry;
    PHYSFS_sint64 retval = 0;
    PHYSFS_sint64 maxread = ((PHYSFS_sint64) objSize) * objCount;
    PHYSFS_sint64 avail = entry->uncompressed_size -
                          finfo->uncompressed_position;

    BAIL_IF_MACRO(maxread == 0, NULL, 0);    /* quick rejection. */

    if (avail < maxread)
    {
        maxread = avail - (avail % objSize);
        objCount = (PHYSFS_uint32) (maxread / objSize);
        BAIL_IF_MACRO(objCount == 0, ERR_PAST_EOF, 0);  /* quick rejection. */
        __PHYSFS_setError(ERR_PAST_EOF);   /* this is always true here. */
    } /* if */

    if (entry->compression_method == COMPMETH_NONE)
    {
        retval = __PHYSFS_platformRead(finfo->handle, buf, objSize, objCount);
    } /* if */

    else
    {
        finfo->stream.next_out = buf;
        finfo->stream.avail_out = objSize * objCount;

        while (retval < maxread)
        {
            PHYSFS_uint32 before = finfo->stream.total_out;
            int rc;

            if (finfo->stream.avail_in == 0)
            {
                PHYSFS_sint64 br;

                br = entry->compressed_size - finfo->compressed_position;
                if (br > 0)
                {
                    if (br > ZIP_READBUFSIZE)
                        br = ZIP_READBUFSIZE;

                    br = __PHYSFS_platformRead(finfo->handle,
                                               finfo->buffer,
                                               1, (PHYSFS_uint32) br);
                    if (br <= 0)
                        break;

                    finfo->compressed_position += (PHYSFS_uint32) br;
                    finfo->stream.next_in = finfo->buffer;
                    finfo->stream.avail_in = (PHYSFS_uint32) br;
                } /* if */
            } /* if */

            rc = zlib_err(inflate(&finfo->stream, Z_SYNC_FLUSH));
            retval += (finfo->stream.total_out - before);

            if (rc != Z_OK)
                break;
        } /* while */

        retval /= objSize;
    } /* else */

    if (retval > 0)
        finfo->uncompressed_position += (PHYSFS_uint32) (retval * objSize);

    return(retval);
} /* ZIP_read */


#if !__MOJOSETUP__
static PHYSFS_sint64 ZIP_write(fvoid *opaque, const void *buf,
                               PHYSFS_uint32 objSize, PHYSFS_uint32 objCount)
{
    BAIL_MACRO(ERR_NOT_SUPPORTED, -1);
} /* ZIP_write */


static int ZIP_eof(fvoid *opaque)
{
    ZIPfileinfo *finfo = (ZIPfileinfo *) opaque;
    return(finfo->uncompressed_position >= finfo->entry->uncompressed_size);
} /* ZIP_eof */
#endif


static PHYSFS_sint64 ZIP_tell(fvoid *opaque)
{
    return(((ZIPfileinfo *) opaque)->uncompressed_position);
} /* ZIP_tell */


static int ZIP_seek(fvoid *opaque, PHYSFS_uint64 offset)
{
    ZIPfileinfo *finfo = (ZIPfileinfo *) opaque;
    ZIPentry *entry = finfo->entry;
    void *in = finfo->handle;

    BAIL_IF_MACRO(offset > entry->uncompressed_size, ERR_PAST_EOF, 0);

    if (entry->compression_method == COMPMETH_NONE)
    {
        PHYSFS_sint64 newpos = offset + entry->offset;
        BAIL_IF_MACRO(!__PHYSFS_platformSeek(in, newpos), NULL, 0);
        finfo->uncompressed_position = (PHYSFS_uint32) offset;
    } /* if */

    else
    {
        /*
         * If seeking backwards, we need to redecode the file
         *  from the start and throw away the compressed bits until we hit
         *  the offset we need. If seeking forward, we still need to
         *  decode, but we don't rewind first.
         */
        if (offset < finfo->uncompressed_position)
        {
            /* we do a copy so state is sane if inflateInit2() fails. */
            z_stream str;
            initializeZStream(&str);
            if (zlib_err(inflateInit2(&str, -MAX_WBITS)) != Z_OK)
                return(0);

            if (!__PHYSFS_platformSeek(in, entry->offset))
                return(0);

            inflateEnd(&finfo->stream);
            memcpy(&finfo->stream, &str, sizeof (z_stream));
            finfo->uncompressed_position = finfo->compressed_position = 0;
        } /* if */

        while (finfo->uncompressed_position != offset)
        {
            PHYSFS_uint8 buf[512];
            PHYSFS_uint32 maxread;

            maxread = (PHYSFS_uint32) (offset - finfo->uncompressed_position);
            if (maxread > sizeof (buf))
                maxread = sizeof (buf);

            if (ZIP_read(opaque, buf, maxread, 1) != 1)
                return(0);
        } /* while */
    } /* else */

    return(1);
} /* ZIP_seek */


static PHYSFS_sint64 ZIP_fileLength(fvoid *opaque)
{
    ZIPfileinfo *finfo = (ZIPfileinfo *) opaque;
    return (PHYSFS_sint64) finfo->entry->uncompressed_size;
} /* ZIP_fileLength */


static int ZIP_fileClose(fvoid *opaque)
{
    ZIPfileinfo *finfo = (ZIPfileinfo *) opaque;
    BAIL_IF_MACRO(!__PHYSFS_platformClose(finfo->handle), NULL, 0);

    if (finfo->entry->compression_method != COMPMETH_NONE)
        inflateEnd(&finfo->stream);

    if (finfo->buffer != NULL)
        allocator.Free(finfo->buffer);

    allocator.Free(finfo);
    return(1);
} /* ZIP_fileClose */


static PHYSFS_sint64 zip_find_end_of_central_dir(void *in, PHYSFS_sint64 *len)
{
    PHYSFS_uint8 buf[256];
    PHYSFS_uint8 extra[4] = { 0, 0, 0, 0 };
    PHYSFS_sint32 i = 0;
    PHYSFS_sint64 filelen;
    PHYSFS_sint64 filepos;
    PHYSFS_sint32 maxread;
    PHYSFS_sint32 totalread = 0;
    int found = 0;

    filelen = __PHYSFS_platformFileLength(in);
    BAIL_IF_MACRO(filelen == -1, NULL, 0);  /* !!! FIXME: unlocalized string */

    /*
     * Jump to the end of the file and start reading backwards.
     *  The last thing in the file is the zipfile comment, which is variable
     *  length, and the field that specifies its size is before it in the
     *  file (argh!)...this means that we need to scan backwards until we
     *  hit the end-of-central-dir signature. We can then sanity check that
     *  the comment was as big as it should be to make sure we're in the
     *  right place. The comment length field is 16 bits, so we can stop
     *  searching for that signature after a little more than 64k at most,
     *  and call it a corrupted zipfile.
     */

    if (sizeof (buf) < filelen)
    {
        filepos = filelen - sizeof (buf);
        maxread = sizeof (buf);
    } /* if */
    else
    {
        filepos = 0;
        maxread = (PHYSFS_uint32) filelen;
    } /* else */

    while ((totalread < filelen) && (totalread < 65557))
    {
        BAIL_IF_MACRO(!__PHYSFS_platformSeek(in, filepos), NULL, -1);

        /* make sure we catch a signature between buffers. */
        if (totalread != 0)
        {
            if (__PHYSFS_platformRead(in, buf, maxread - 4, 1) != 1)
                return(-1);
            memcpy(&buf[maxread - 4], &extra, sizeof (extra));
            totalread += maxread - 4;
        } /* if */
        else
        {
            if (__PHYSFS_platformRead(in, buf, maxread, 1) != 1)
                return(-1);
            totalread += maxread;
        } /* else */

        memcpy(&extra, buf, sizeof (extra));

        for (i = maxread - 4; i > 0; i--)
        {
            if ((buf[i + 0] == 0x50) &&
                (buf[i + 1] == 0x4B) &&
                (buf[i + 2] == 0x05) &&
                (buf[i + 3] == 0x06) )
            {
                found = 1;  /* that's the signature! */
                break;  
            } /* if */
        } /* for */

        if (found)
            break;

        filepos -= (maxread - 4);
        if (filepos < 0)
            filepos = 0;
    } /* while */

    BAIL_IF_MACRO(!found, ERR_NOT_AN_ARCHIVE, -1);

    if (len != NULL)
        *len = filelen;

    return(filepos + i);
} /* zip_find_end_of_central_dir */


#if !__MOJOSETUP__
static int ZIP_isArchive(const char *filename, int forWriting)
{
    PHYSFS_uint32 sig;
    int retval = 0;
    void *in;

    in = __PHYSFS_platformOpenRead(filename);
    BAIL_IF_MACRO(in == NULL, NULL, 0);

    /*
     * The first thing in a zip file might be the signature of the
     *  first local file record, so it makes for a quick determination.
     */
    if (readui32(in, &sig))
    {
        retval = (sig == ZIP_LOCAL_FILE_SIG);
        if (!retval)
        {
            /*
             * No sig...might be a ZIP with data at the start
             *  (a self-extracting executable, etc), so we'll have to do
             *  it the hard way...
             */
            retval = (zip_find_end_of_central_dir(in, NULL) != -1);
        } /* if */
    } /* if */

    __PHYSFS_platformClose(in);
    return(retval);
} /* ZIP_isArchive */
#endif

static void zip_free_entries(ZIPentry *entries, PHYSFS_uint64 max)
{
    PHYSFS_uint64 i;
    for (i = 0; i < max; i++)
    {
        ZIPentry *entry = &entries[i];
        if (entry->name != NULL)
            allocator.Free(entry->name);
        #if __MOJOSETUP__
        if (entry->linkdest != NULL)
            allocator.Free(entry->linkdest);
        #endif
    } /* for */

    allocator.Free(entries);
} /* zip_free_entries */


/*
 * This will find the ZIPentry associated with a path in platform-independent
 *  notation. Directories don't have ZIPentries associated with them, but 
 *  (*isDir) will be set to non-zero if a dir was hit.
 */
static ZIPentry *zip_find_entry(ZIPinfo *info, const char *path, int *isDir)
{
    ZIPentry *a = info->entries;
    PHYSFS_sint32 pathlen = strlen(path);
    PHYSFS_sint64 lo = 0;
    PHYSFS_sint64 hi = (PHYSFS_sint64) (info->entryCount - 1);
    PHYSFS_sint64 middle;
    const char *thispath = NULL;
    int rc;

    while (lo <= hi)
    {
        middle = lo + ((hi - lo) / 2);
        thispath = a[middle].name;
        rc = strncmp(path, thispath, pathlen);

        if (rc > 0)
            lo = middle + 1;

        else if (rc < 0)
            hi = middle - 1;

        else /* substring match...might be dir or entry or nothing. */
        {
            if (isDir != NULL)
            {
                *isDir = (thispath[pathlen] == '/');
                if (*isDir)
                    return(NULL);
            } /* if */

            if (thispath[pathlen] == '\0') /* found entry? */
                return(&a[middle]);
            else
                hi = middle - 1;  /* adjust search params, try again. */
        } /* if */
    } /* while */

    if (isDir != NULL)
        *isDir = 0;

    BAIL_MACRO(ERR_NO_SUCH_FILE, NULL);
} /* zip_find_entry */


/* Convert paths from old, buggy DOS zippers... */
static void zip_convert_dos_path(ZIPentry *entry, char *path)
{
    PHYSFS_uint8 hosttype = (PHYSFS_uint8) ((entry->version >> 8) & 0xFF);
    if (hosttype == 0)  /* FS_FAT_ */
    {
        while (*path)
        {
            if (*path == '\\')
                *path = '/';
            path++;
        } /* while */
    } /* if */
} /* zip_convert_dos_path */


static void zip_expand_symlink_path(char *path)
{
    char *ptr = path;
    char *prevptr = path;

    while (1)
    {
        ptr = strchr(ptr, '/');
        if (ptr == NULL)
            break;

        if (*(ptr + 1) == '.')
        {
            if (*(ptr + 2) == '/')
            {
                /* current dir in middle of string: ditch it. */
                memmove(ptr, ptr + 2, strlen(ptr + 2) + 1);
            } /* else if */

            else if (*(ptr + 2) == '\0')
            {
                /* current dir at end of string: ditch it. */
                *ptr = '\0';
            } /* else if */

            else if (*(ptr + 2) == '.')
            {
                if (*(ptr + 3) == '/')
                {
                    /* parent dir in middle: move back one, if possible. */
                    memmove(prevptr, ptr + 4, strlen(ptr + 4) + 1);
                    ptr = prevptr;
                    while (prevptr != path)
                    {
                        prevptr--;
                        if (*prevptr == '/')
                        {
                            prevptr++;
                            break;
                        } /* if */
                    } /* while */
                } /* if */

                if (*(ptr + 3) == '\0')
                {
                    /* parent dir at end: move back one, if possible. */
                    *prevptr = '\0';
                } /* if */
            } /* if */
        } /* if */
        else
        {
            prevptr = ptr;
            ptr++;
        } /* else */
    } /* while */
} /* zip_expand_symlink_path */

/* (forward reference: zip_follow_symlink and zip_resolve call each other.) */
static int zip_resolve(void *in, ZIPinfo *info, ZIPentry *entry);

/*
 * Look for the entry named by (path). If it exists, resolve it, and return
 *  a pointer to that entry. If it's another symlink, keep resolving until you
 *  hit a real file and then return a pointer to the final non-symlink entry.
 *  If there's a problem, return NULL. (path) is always free()'d by this
 *  function.
 */
static ZIPentry *zip_follow_symlink(void *in, ZIPinfo *info, char *path)
{
    ZIPentry *entry;

    zip_expand_symlink_path(path);
    entry = zip_find_entry(info, path, NULL);
    if (entry != NULL)
    {
        if (!zip_resolve(in, info, entry))  /* recursive! */
            entry = NULL;
        else
        {
            if (entry->symlink != NULL)
                entry = entry->symlink;
        } /* else */
    } /* if */

    allocator.Free(path);
    return(entry);
} /* zip_follow_symlink */


static int zip_resolve_symlink(void *in, ZIPinfo *info, ZIPentry *entry)
{
    char *path;
    const PHYSFS_uint64 size = entry->uncompressed_size;
    int rc = 0;

    /*
     * We've already parsed the local file header of the symlink at this
     *  point. Now we need to read the actual link from the file data and
     *  follow it.
     */

    BAIL_IF_MACRO(!__PHYSFS_platformSeek(in, entry->offset), NULL, 0);

    path = (char *) allocator.Malloc(size + 1);
    BAIL_IF_MACRO(path == NULL, ERR_OUT_OF_MEMORY, 0);
    
    if (entry->compression_method == COMPMETH_NONE)
        rc = (__PHYSFS_platformRead(in, path, size, 1) == 1);

    else  /* symlink target path is compressed... */
    {
        z_stream stream;
        const PHYSFS_uint64 compsize = entry->compressed_size;
        PHYSFS_uint8 *compressed = (PHYSFS_uint8 *) allocator.Malloc(compsize);
        if (compressed != NULL)
        {
            if (__PHYSFS_platformRead(in, compressed, compsize, 1) == 1)
            {
                initializeZStream(&stream);
                stream.next_in = compressed;
                stream.avail_in = compsize;
                stream.next_out = (unsigned char *) path;
                stream.avail_out = size;
                if (zlib_err(inflateInit2(&stream, -MAX_WBITS)) == Z_OK)
                {
                    rc = zlib_err(inflate(&stream, Z_FINISH));
                    inflateEnd(&stream);

                    /* both are acceptable outcomes... */
                    rc = ((rc == Z_OK) || (rc == Z_STREAM_END));
                } /* if */
            } /* if */
            allocator.Free(compressed);
        } /* if */
    } /* else */

    if (!rc)
        allocator.Free(path);
    else
    {
        path[entry->uncompressed_size] = '\0';    /* null-terminate it. */
        zip_convert_dos_path(entry, path);
        #if __MOJOSETUP__
        entry->linkdest = xstrdup(path);
        #endif
        entry->symlink = zip_follow_symlink(in, info, path);
    } /* else */

    return(entry->symlink != NULL);
} /* zip_resolve_symlink */


/*
 * Parse the local file header of an entry, and update entry->offset.
 */
static int zip_parse_local(void *in, ZIPentry *entry)
{
    PHYSFS_uint32 ui32;
    PHYSFS_uint16 ui16;
    PHYSFS_uint16 fnamelen;
    PHYSFS_uint16 extralen;

    /*
     * crc and (un)compressed_size are always zero if this is a "JAR"
     *  archive created with Sun's Java tools, apparently. We only
     *  consider this archive corrupted if those entries don't match and
     *  aren't zero. That seems to work well.
     * We also ignore a mismatch if the value is 0xFFFFFFFF here, since it's
     *  possible that's a Zip64 thing.
     */

    BAIL_IF_MACRO(!__PHYSFS_platformSeek(in, entry->offset), NULL, 0);
    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);
    BAIL_IF_MACRO(ui32 != ZIP_LOCAL_FILE_SIG, ERR_CORRUPTED, 0);
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);
    BAIL_IF_MACRO(ui16 != entry->version_needed, ERR_CORRUPTED, 0);
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);  /* general bits. */
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);
    BAIL_IF_MACRO(ui16 != entry->compression_method, ERR_CORRUPTED, 0);
    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);  /* date/time */
    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);
    BAIL_IF_MACRO(ui32 && (ui32 != entry->crc), ERR_CORRUPTED, 0);

    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);
    BAIL_IF_MACRO(ui32 && (ui32 != 0xFFFFFFFF) &&
                  (ui32 != entry->compressed_size), PHYSFS_ERR_CORRUPT, 0);

    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);
    BAIL_IF_MACRO(ui32 && (ui32 != 0xFFFFFFFF) &&
                 (ui32 != entry->uncompressed_size), PHYSFS_ERR_CORRUPT, 0);

    BAIL_IF_MACRO(!readui16(in, &fnamelen), NULL, 0);
    BAIL_IF_MACRO(!readui16(in, &extralen), NULL, 0);

    entry->offset += fnamelen + extralen + 30;
    return(1);
} /* zip_parse_local */


static int zip_resolve(void *in, ZIPinfo *info, ZIPentry *entry)
{
    int retval = 1;
    ZipResolveType resolve_type = entry->resolved;

    /* Don't bother if we've failed to resolve this entry before. */
    BAIL_IF_MACRO(resolve_type == ZIP_BROKEN_FILE, ERR_CORRUPTED, 0);
    BAIL_IF_MACRO(resolve_type == ZIP_BROKEN_SYMLINK, ERR_CORRUPTED, 0);

    /* uhoh...infinite symlink loop! */
    BAIL_IF_MACRO(resolve_type == ZIP_RESOLVING, ERR_SYMLINK_LOOP, 0);

    /*
     * We fix up the offset to point to the actual data on the
     *  first open, since we don't want to seek across the whole file on
     *  archive open (can be SLOW on large, CD-stored files), but we
     *  need to check the local file header...not just for corruption,
     *  but since it stores offset info the central directory does not.
     */
    if (resolve_type != ZIP_RESOLVED)
    {
        entry->resolved = ZIP_RESOLVING;

        retval = zip_parse_local(in, entry);
        if (retval)
        {
            /*
             * If it's a symlink, find the original file. This will cause
             *  resolution of other entries (other symlinks and, eventually,
             *  the real file) if all goes well.
             */
            if (resolve_type == ZIP_UNRESOLVED_SYMLINK)
                retval = zip_resolve_symlink(in, info, entry);
        } /* if */

        if (resolve_type == ZIP_UNRESOLVED_SYMLINK)
            entry->resolved = ((retval) ? ZIP_RESOLVED : ZIP_BROKEN_SYMLINK);
        else if (resolve_type == ZIP_UNRESOLVED_FILE)
            entry->resolved = ((retval) ? ZIP_RESOLVED : ZIP_BROKEN_FILE);
    } /* if */

    return(retval);
} /* zip_resolve */


static int zip_version_does_symlinks(PHYSFS_uint32 version)
{
    int retval = 0;
    PHYSFS_uint8 hosttype = (PHYSFS_uint8) ((version >> 8) & 0xFF);

    switch (hosttype)
    {
            /*
             * These are the platforms that can NOT build an archive with
             *  symlinks, according to the Info-ZIP project.
             */
        case 0:  /* FS_FAT_  */
        case 1:  /* AMIGA_   */
        case 2:  /* VMS_     */
        case 4:  /* VM_CSM_  */
        case 6:  /* FS_HPFS_ */
        case 11: /* FS_NTFS_ */
        case 14: /* FS_VFAT_ */
        case 13: /* ACORN_   */
        case 15: /* MVS_     */
        case 18: /* THEOS_   */
            break;  /* do nothing. */

        default:  /* assume the rest to be unix-like. */
            retval = 1;
            break;
    } /* switch */

    return(retval);
} /* zip_version_does_symlinks */


static int zip_entry_is_symlink(const ZIPentry *entry)
{
    return((entry->resolved == ZIP_UNRESOLVED_SYMLINK) ||
           (entry->resolved == ZIP_BROKEN_SYMLINK) ||
           (entry->symlink));
} /* zip_entry_is_symlink */


static int zip_has_symlink_attr(ZIPentry *entry, PHYSFS_uint32 extern_attr)
{
    PHYSFS_uint16 xattr = ((extern_attr >> 16) & 0xFFFF);

    return (
              (zip_version_does_symlinks(entry->version)) &&
              (entry->uncompressed_size > 0) &&
              ((xattr & UNIX_FILETYPE_MASK) == UNIX_FILETYPE_SYMLINK)
           );
} /* zip_has_symlink_attr */


static PHYSFS_sint64 zip_dos_time_to_physfs_time(PHYSFS_uint32 dostime)
{
#ifdef _WIN32_WCE
    /* We have no struct tm and no mktime right now.
       FIXME: This should probably be fixed at some point.
    */
    return -1;
#else
    PHYSFS_uint32 dosdate;
    struct tm unixtime;
    memset(&unixtime, '\0', sizeof (unixtime));

    dosdate = (PHYSFS_uint32) ((dostime >> 16) & 0xFFFF);
    dostime &= 0xFFFF;

    /* dissect date */
    unixtime.tm_year = ((dosdate >> 9) & 0x7F) + 80;
    unixtime.tm_mon  = ((dosdate >> 5) & 0x0F) - 1;
    unixtime.tm_mday = ((dosdate     ) & 0x1F);

    /* dissect time */
    unixtime.tm_hour = ((dostime >> 11) & 0x1F);
    unixtime.tm_min  = ((dostime >>  5) & 0x3F);
    unixtime.tm_sec  = ((dostime <<  1) & 0x3E);

    /* let mktime calculate daylight savings time. */
    unixtime.tm_isdst = -1;

    return((PHYSFS_sint64) mktime(&unixtime));
#endif
} /* zip_dos_time_to_physfs_time */


static int zip_load_entry(void *in, const int zip64, ZIPentry *entry,
                          PHYSFS_uint64 ofs_fixup)
{
    PHYSFS_uint16 fnamelen, extralen, commentlen;
    PHYSFS_uint32 external_attr;
    PHYSFS_uint32 starting_disk;
    PHYSFS_uint64 offset;
    PHYSFS_uint16 ui16;
    PHYSFS_uint32 ui32;
    PHYSFS_sint64 si64;

    /* sanity check with central directory signature... */
    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);
    BAIL_IF_MACRO(ui32 != ZIP_CENTRAL_DIR_SIG, ERR_CORRUPTED, 0);

    /* Get the pertinent parts of the record... */
    BAIL_IF_MACRO(!readui16(in, &entry->version), NULL, 0);
    BAIL_IF_MACRO(!readui16(in, &entry->version_needed), NULL, 0);
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);  /* general bits */
    BAIL_IF_MACRO(!readui16(in, &entry->compression_method), NULL, 0);
    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);
    entry->last_mod_time = zip_dos_time_to_physfs_time(ui32);
    BAIL_IF_MACRO(!readui32(in, &entry->crc), NULL, 0);
    BAIL_IF_MACRO(!readui32(in, &ui32), ERRPASS, 0);
    entry->compressed_size = (PHYSFS_uint64) ui32;
    BAIL_IF_MACRO(!readui32(in, &ui32), ERRPASS, 0);
    entry->uncompressed_size = (PHYSFS_uint64) ui32;
    BAIL_IF_MACRO(!readui16(in, &fnamelen), NULL, 0);
    BAIL_IF_MACRO(!readui16(in, &extralen), NULL, 0);
    BAIL_IF_MACRO(!readui16(in, &commentlen), NULL, 0);
    BAIL_IF_MACRO(!readui16(in, &ui16), ERRPASS, 0);
    starting_disk = (PHYSFS_uint32) ui16;
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);  /* internal file attribs */
    BAIL_IF_MACRO(!readui32(in, &external_attr), NULL, 0);
    BAIL_IF_MACRO(!readui32(in, &ui32), ERRPASS, 0);
    offset = (PHYSFS_uint64) ui32;

    #if __MOJOSETUP__
    entry->perms = (external_attr >> 16) & 0xFFFF;
    entry->linkdest = NULL;
    #endif

    entry->symlink = NULL;  /* will be resolved later, if necessary. */
    entry->resolved = (zip_has_symlink_attr(entry, external_attr)) ?
                            ZIP_UNRESOLVED_SYMLINK : ZIP_UNRESOLVED_FILE;

    entry->name = (char *) allocator.Malloc(fnamelen + 1);
    BAIL_IF_MACRO(entry->name == NULL, ERR_OUT_OF_MEMORY, 0);
    if (__PHYSFS_platformRead(in, entry->name, fnamelen, 1) != 1)
        goto zip_load_entry_puked;

    entry->name[fnamelen] = '\0';  /* null-terminate the filename. */
    zip_convert_dos_path(entry, entry->name);

    si64 = __PHYSFS_platformTell(in);
    if (si64 == -1)
        goto zip_load_entry_puked;

    /*
     * The actual sizes didn't fit in 32-bits; look for the Zip64
     *  extended information extra field...
     */
    if ( (zip64) &&
         ((offset == 0xFFFFFFFF) ||
          (starting_disk == 0xFFFFFFFF) ||
          (entry->compressed_size == 0xFFFFFFFF) ||
          (entry->uncompressed_size == 0xFFFFFFFF)) )
    {
        int found = 0;
        PHYSFS_uint16 sig, len;
        while (extralen > 4)
        {
            if (!readui16(in, &sig))
                goto zip_load_entry_puked;
            else if (!readui16(in, &len))
                goto zip_load_entry_puked;

            si64 += 4 + len;
            extralen -= 4 + len;
            if (sig != ZIP64_EXTENDED_INFO_EXTRA_FIELD_SIG)
            {
                if (!__PHYSFS_platformSeek(in, si64))
                    goto zip_load_entry_puked;
                continue;
            } /* if */

            found = 1;
            break;
        } /* while */

        GOTO_IF_MACRO(!found, PHYSFS_ERR_CORRUPT, zip_load_entry_puked);

        if (entry->uncompressed_size == 0xFFFFFFFF)
        {
            GOTO_IF_MACRO(len < 8, PHYSFS_ERR_CORRUPT, zip_load_entry_puked);
            if (!readui64(in, &entry->uncompressed_size))
                goto zip_load_entry_puked;
            len -= 8;
        } /* if */

        if (entry->compressed_size == 0xFFFFFFFF)
        {
            GOTO_IF_MACRO(len < 8, PHYSFS_ERR_CORRUPT, zip_load_entry_puked);
            if (!readui64(in, &entry->compressed_size))
                goto zip_load_entry_puked;
            len -= 8;
        } /* if */

        if (offset == 0xFFFFFFFF)
        {
            GOTO_IF_MACRO(len < 8, PHYSFS_ERR_CORRUPT, zip_load_entry_puked);
            if (!readui64(in, &offset))
                goto zip_load_entry_puked;
            len -= 8;
        } /* if */

        if (starting_disk == 0xFFFFFFFF)
        {
            GOTO_IF_MACRO(len < 8, PHYSFS_ERR_CORRUPT, zip_load_entry_puked);
            if (!readui32(in, &starting_disk))
                goto zip_load_entry_puked;
            len -= 4;
        } /* if */

        GOTO_IF_MACRO(len != 0, PHYSFS_ERR_CORRUPT, zip_load_entry_puked);
    } /* if */

    GOTO_IF_MACRO(starting_disk != 0, PHYSFS_ERR_CORRUPT, zip_load_entry_puked);

    entry->offset = offset + ofs_fixup;

    /* seek to the start of the next entry in the central directory... */
    if (!__PHYSFS_platformSeek(in, si64 + extralen + commentlen))
        goto zip_load_entry_puked;

    return(1);  /* success. */

zip_load_entry_puked:
    allocator.Free(entry->name);
    return(0);  /* failure. */
} /* zip_load_entry */


static int zip_entry_cmp(void *_a, size_t one, size_t two)
{
    if (one != two)
    {
        const ZIPentry *a = (const ZIPentry *) _a;
        return(strcmp(a[one].name, a[two].name));
    } /* if */

    return 0;
} /* zip_entry_cmp */


static void zip_entry_swap(void *_a, size_t one, size_t two)
{
    if (one != two)
    {
        ZIPentry tmp;
        ZIPentry *first = &(((ZIPentry *) _a)[one]);
        ZIPentry *second = &(((ZIPentry *) _a)[two]);
        memcpy(&tmp, first, sizeof (ZIPentry));
        memcpy(first, second, sizeof (ZIPentry));
        memcpy(second, &tmp, sizeof (ZIPentry));
    } /* if */
} /* zip_entry_swap */


static int zip_load_entries(void *in, ZIPinfo *info,
                            const PHYSFS_uint64 data_ofs,
                            const PHYSFS_uint64 central_ofs)
{
    const PHYSFS_uint64 max = info->entryCount;
    const int zip64 = info->zip64;
    PHYSFS_uint64 i;

    BAIL_IF_MACRO(!__PHYSFS_platformSeek(in, central_ofs), NULL, 0);

    info->entries = (ZIPentry *) allocator.Malloc(sizeof (ZIPentry) * max);
    BAIL_IF_MACRO(info->entries == NULL, ERR_OUT_OF_MEMORY, 0);

    for (i = 0; i < max; i++)
    {
        if (!zip_load_entry(in, zip64, &info->entries[i], data_ofs))
        {
            zip_free_entries(info->entries, i);
            return(0);
        } /* if */
    } /* for */

    __PHYSFS_sort(info->entries, (size_t) max, zip_entry_cmp, zip_entry_swap);
    return(1);
} /* zip_load_entries */


static PHYSFS_sint64 zip64_find_end_of_central_dir(void *io,
                                                   PHYSFS_sint64 _pos,
                                                   PHYSFS_uint64 offset)
{
    /*
     * Naturally, the offset is useless to us; it is the offset from the
     *  start of file, which is meaningless if we've appended this .zip to
     *  a self-extracting .exe. We need to find this on our own. It should
     *  be directly before the locator record, but the record in question,
     *  like the original end-of-central-directory record, ends with a
     *  variable-length field. Unlike the original, which has to store the
     *  size of that variable-length field in a 16-bit int and thus has to be
     *  within 64k, the new one gets 64-bits.
     *
     * Fortunately, the only currently-specified record for that variable
     *  length block is some weird proprietary thing that deals with EBCDIC
     *  and tape backups or something. So we don't seek far.
     */

    PHYSFS_uint32 ui32;
    const PHYSFS_uint64 pos = (PHYSFS_uint64) _pos;

    assert(_pos > 0);

    /* Try offset specified in the Zip64 end of central directory locator. */
    /* This works if the entire PHYSFS_Io is the zip file. */
    BAIL_IF_MACRO(!__PHYSFS_platformSeek(io, offset), NULL, 0);
    BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, -1);
    if (ui32 == ZIP64_END_OF_CENTRAL_DIR_SIG)
        return offset;

    /* Try 56 bytes before the Zip64 end of central directory locator. */
    /* This works if the record isn't variable length and is version 1. */
    if (pos > 56)
    {
        BAIL_IF_MACRO(!__PHYSFS_platformSeek(io, pos-56), ERRPASS, -1);
        BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, -1);
        if (ui32 == ZIP64_END_OF_CENTRAL_DIR_SIG)
            return pos-56;
    } /* if */

    /* Try 84 bytes before the Zip64 end of central directory locator. */
    /* This works if the record isn't variable length and is version 2. */
    if (pos > 84)
    {
        BAIL_IF_MACRO(!__PHYSFS_platformSeek(io, pos-84), ERRPASS, -1);
        BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, -1);
        if (ui32 == ZIP64_END_OF_CENTRAL_DIR_SIG)
            return pos-84;
    } /* if */

    /* Ok, brute force: we know it's between (offset) and (pos) somewhere. */
    /*  Just try moving back at most 256k. Oh well. */
    if ((offset < pos) && (pos > 4))
    {
        /* we assume you can eat this stack if you handle Zip64 files. */
        PHYSFS_uint8 buf[256 * 1024];
        PHYSFS_uint64 len = pos - offset;
        PHYSFS_uint32 i;

        if (len > sizeof (buf))
            len = sizeof (buf);

        BAIL_IF_MACRO(!__PHYSFS_platformSeek(io, pos - len), ERRPASS, -1);
        BAIL_IF_MACRO(!__PHYSFS_readAll(io, buf, len), ERRPASS, -1);
        for (i = len - 4; i >= 0; i--)
        {
            if (buf[i] != 0x50)
                continue;
            if ( (buf[i+1] == 0x4b) &&
                 (buf[i+2] == 0x06) &&
                 (buf[i+3] == 0x06) )
                return pos - (len - i);
        } /* for */
    } /* if */

    BAIL_MACRO(PHYSFS_ERR_CORRUPT, -1);  /* didn't find it. */
} /* zip64_find_end_of_central_dir */


static int zip64_parse_end_of_central_dir(void *io, ZIPinfo *info,
                                          PHYSFS_uint64 *data_start,
                                          PHYSFS_uint64 *dir_ofs,
                                          PHYSFS_sint64 pos)
{
    PHYSFS_uint64 ui64;
    PHYSFS_uint32 ui32;
    PHYSFS_uint16 ui16;
    //PHYSFS_sint64 len;

    /* We should be positioned right past the locator signature. */

    if ((pos < 0) || (!__PHYSFS_platformSeek(io, pos)))
        return 0;

    BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, 0);
    if (ui32 != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG)
        return -1;  /* it's not a Zip64 archive. Not an error, though! */

    info->zip64 = 1;

    /* number of the disk with the start of the central directory. */
    BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, 0);
    BAIL_IF_MACRO(ui32 != 0, PHYSFS_ERR_CORRUPT, 0);

    /* offset of Zip64 end of central directory record. */
    BAIL_IF_MACRO(!readui64(io, &ui64), ERRPASS, 0);

    /* total number of disks */
    BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, 0);
    BAIL_IF_MACRO(ui32 != 1, PHYSFS_ERR_CORRUPT, 0);

    pos = zip64_find_end_of_central_dir(io, pos, ui64);
    if (pos < 0)
        return 0;  /* oh well. */

    /*
     * For self-extracting archives, etc, there's crapola in the file
     *  before the zipfile records; we calculate how much data there is
     *  prepended by determining how far the zip64-end-of-central-directory
     *  offset is from where it is supposed to be...the difference in bytes
     *  is how much arbitrary data is at the start of the physical file.
     */
    assert(((PHYSFS_uint64) pos) >= ui64);
    *data_start = ((PHYSFS_uint64) pos) - ui64;

    BAIL_IF_MACRO(!__PHYSFS_platformSeek(io, pos), ERRPASS, 0);

    /* check signature again, just in case. */
    BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, 0);
    BAIL_IF_MACRO(ui32 != ZIP64_END_OF_CENTRAL_DIR_SIG, PHYSFS_ERR_CORRUPT, 0);

    /* size of Zip64 end of central directory record. */
    BAIL_IF_MACRO(!readui64(io, &ui64), ERRPASS, 0);

    /* version made by. */
    BAIL_IF_MACRO(!readui16(io, &ui16), ERRPASS, 0);

    /* version needed to extract. */
    BAIL_IF_MACRO(!readui16(io, &ui16), ERRPASS, 0);

    /* number of this disk. */
    BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, 0);
    BAIL_IF_MACRO(ui32 != 0, PHYSFS_ERR_CORRUPT, 0);

    /* number of disk with start of central directory record. */
    BAIL_IF_MACRO(!readui32(io, &ui32), ERRPASS, 0);
    BAIL_IF_MACRO(ui32 != 0, PHYSFS_ERR_CORRUPT, 0);

    /* total number of entries in the central dir on this disk */
    BAIL_IF_MACRO(!readui64(io, &ui64), ERRPASS, 0);

    /* total number of entries in the central dir */
    BAIL_IF_MACRO(!readui64(io, &info->entryCount), ERRPASS, 0);
    BAIL_IF_MACRO(ui64 != info->entryCount, PHYSFS_ERR_CORRUPT, 0);

    /* size of the central directory */
    BAIL_IF_MACRO(!readui64(io, &ui64), ERRPASS, 0);

    /* offset of central directory */
    BAIL_IF_MACRO(!readui64(io, dir_ofs), ERRPASS, 0);

    /* Since we know the difference, fix up the central dir offset... */
    *dir_ofs += *data_start;

    /*
     * There are more fields here, for encryption and feature-specific things,
     *  but we don't care about any of them at the moment.
     */

    return 1;  /* made it. */
} /* zip64_parse_end_of_central_dir */


static int zip_parse_end_of_central_dir(void *in, ZIPinfo *info,
                                        PHYSFS_uint64 *data_start,
                                        PHYSFS_uint64 *dir_ofs)
 {
    PHYSFS_uint16 entryCount16;
    PHYSFS_uint32 offset32;
    PHYSFS_uint32 ui32;
    PHYSFS_uint16 ui16;
    PHYSFS_sint64 len = 0;
    PHYSFS_sint64 pos;
    int rc;

    /* find the end-of-central-dir record, and seek to it. */
    pos = zip_find_end_of_central_dir(in, &len);
    BAIL_IF_MACRO(pos == -1, NULL, 0);
    BAIL_IF_MACRO(!__PHYSFS_platformSeek(in, pos), NULL, 0);

    /* check signature again, just in case. */
    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);
    BAIL_IF_MACRO(ui32 != ZIP_END_OF_CENTRAL_DIR_SIG, ERR_NOT_AN_ARCHIVE, 0);

    /* Seek back to see if "Zip64 end of central directory locator" exists. */
    /* this record is 20 bytes before end-of-central-dir */
    rc = zip64_parse_end_of_central_dir(in, info, data_start, dir_ofs, pos-20);
    BAIL_IF_MACRO(rc == 0, ERRPASS, 0);
    if (rc == 1)
        return 1;  /* we're done here. */

    assert(rc == -1);  /* no error, just not a Zip64 archive. */

    /* Not Zip64? Seek back to where we were and keep processing. */
    BAIL_IF_MACRO(!__PHYSFS_platformSeek(in, pos + 4), ERRPASS, 0);

    /* number of this disk */
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);
    BAIL_IF_MACRO(ui16 != 0, ERR_UNSUPPORTED_ARCHIVE, 0);

    /* number of the disk with the start of the central directory */
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);
    BAIL_IF_MACRO(ui16 != 0, ERR_UNSUPPORTED_ARCHIVE, 0);

    /* total number of entries in the central dir on this disk */
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);

    /* total number of entries in the central dir */
    BAIL_IF_MACRO(!readui16(in, &entryCount16), ERRPASS, 0);
    BAIL_IF_MACRO(ui16 != entryCount16, PHYSFS_ERR_CORRUPT, 0);

    info->entryCount = entryCount16;

    /* size of the central directory */
    BAIL_IF_MACRO(!readui32(in, &ui32), NULL, 0);

    /* offset of central directory */
    BAIL_IF_MACRO(!readui32(in, &offset32), ERRPASS, 0);
    *dir_ofs = (PHYSFS_uint64) offset32;
    BAIL_IF_MACRO(pos < (*dir_ofs + ui32), PHYSFS_ERR_CORRUPT, 0);

    /*
     * For self-extracting archives, etc, there's crapola in the file
     *  before the zipfile records; we calculate how much data there is
     *  prepended by determining how far the central directory offset is
     *  from where it is supposed to be (start of end-of-central-dir minus
     *  sizeof central dir)...the difference in bytes is how much arbitrary
     *  data is at the start of the physical file.
     */
    *data_start = (PHYSFS_uint64) (pos - (*dir_ofs + ui32));

    /* Now that we know the difference, fix up the central dir offset... */
    *dir_ofs += *data_start;

    /* zipfile comment length */
    BAIL_IF_MACRO(!readui16(in, &ui16), NULL, 0);

    /*
     * Make sure that the comment length matches to the end of file...
     *  If it doesn't, we're either in the wrong part of the file, or the
     *  file is corrupted, but we give up either way.
     */
    BAIL_IF_MACRO((pos + 22 + ui16) != len, ERR_UNSUPPORTED_ARCHIVE, 0);

    return(1);  /* made it. */
} /* zip_parse_end_of_central_dir */


#if __MOJOSETUP__
static ZIPinfo *zip_create_zipinfo(void *in, const char *name)
#else
static ZIPinfo *zip_create_zipinfo(const char *name)
#endif
{
    char *ptr;
    ZIPinfo *info = (ZIPinfo *) allocator.Malloc(sizeof (ZIPinfo));
    BAIL_IF_MACRO(info == NULL, ERR_OUT_OF_MEMORY, 0);
    memset(info, '\0', sizeof (ZIPinfo));

    ptr = (char *) allocator.Malloc(strlen(name) + 1);
    if (ptr == NULL)
    {
        allocator.Free(info);
        BAIL_MACRO(ERR_OUT_OF_MEMORY, NULL);
    } /* if */

    info->archiveName = ptr;
    strcpy(info->archiveName, name);
#if __MOJOSETUP__
    info->io = in;
    info->enumIndex = -1;
#endif
    return(info);
} /* zip_create_zipinfo */


#if __MOJOSETUP__
static void *ZIP_openArchive(void *in, const char *name, int forWriting)
#else
static void *ZIP_openArchive(const char *name, int forWriting)
#endif
{
#if !__MOJOSETUP__
    void *in = NULL;
#endif

    ZIPinfo *info = NULL;
    PHYSFS_uint64 data_start;
    PHYSFS_uint64 cent_dir_ofs;

    BAIL_IF_MACRO(forWriting, ERR_ARC_IS_READ_ONLY, NULL);

#if __MOJOSETUP__
    if ((info = zip_create_zipinfo(in, name)) == NULL)
        goto zip_openarchive_failed;
#else
    if ((in = __PHYSFS_platformOpenRead(name)) == NULL)
        goto zip_openarchive_failed;
    if ((info = zip_create_zipinfo(name)) == NULL)
        goto zip_openarchive_failed;
#endif

    if (!zip_parse_end_of_central_dir(in, info, &data_start, &cent_dir_ofs))
        goto zip_openarchive_failed;

#if __MOJOSETUP__
    info->offset = (int64) data_start;
#endif

    if (!zip_load_entries(in, info, data_start, cent_dir_ofs))
        goto zip_openarchive_failed;

#if !__MOJOSETUP__
    __PHYSFS_platformClose(in);
#endif

    return(info);

zip_openarchive_failed:
    if (info != NULL)
    {
        if (info->archiveName != NULL)
            allocator.Free(info->archiveName);
        allocator.Free(info);
    } /* if */

#if !__MOJOSETUP__
    if (in != NULL)
        __PHYSFS_platformClose(in);
#endif

    return(NULL);
} /* ZIP_openArchive */


#if !__MOJOSETUP__
static PHYSFS_sint64 zip_find_start_of_dir(ZIPinfo *info, const char *path,
                                            int stop_on_first_find)
{
    PHYSFS_sint64 lo = 0;
    PHYSFS_sint64 hi = (PHYSFS_sint32) (info->entryCount - 1);
    PHYSFS_sint64 middle;
    PHYSFS_uint32 dlen = strlen(path);
    PHYSFS_sint64 retval = -1;
    const char *name;
    int rc;

    if (*path == '\0')  /* root dir? */
        return(0);

    if ((dlen > 0) && (path[dlen - 1] == '/')) /* ignore trailing slash. */
        dlen--;

    while (lo <= hi)
    {
        middle = lo + ((hi - lo) / 2);
        name = info->entries[middle].name;
        rc = strncmp(path, name, dlen);
        if (rc == 0)
        {
            char ch = name[dlen];
            if ('/' < ch) /* make sure this isn't just a substr match. */
                rc = -1;
            else if ('/' > ch)
                rc = 1;
            else 
            {
                if (stop_on_first_find) /* Just checking dir's existance? */
                    return(middle);

                if (name[dlen + 1] == '\0') /* Skip initial dir entry. */
                    return(middle + 1);

                /* there might be more entries earlier in the list. */
                retval = middle;
                hi = middle - 1;
            } /* else */
        } /* if */

        if (rc > 0)
            lo = middle + 1;
        else
            hi = middle - 1;
    } /* while */

    return(retval);
} /* zip_find_start_of_dir */


/*
 * Moved to seperate function so we can use alloca then immediately throw
 *  away the allocated stack space...
 */
static void doEnumCallback(PHYSFS_EnumFilesCallback cb, void *callbackdata,
                           const char *odir, const char *str, PHYSFS_sint32 ln)
{
    char *newstr = alloca(ln + 1);
    if (newstr == NULL)
        return;

    memcpy(newstr, str, ln);
    newstr[ln] = '\0';
    cb(callbackdata, odir, newstr);
} /* doEnumCallback */


static void ZIP_enumerateFiles(dvoid *opaque, const char *dname,
                               int omitSymLinks, PHYSFS_EnumFilesCallback cb,
                               const char *origdir, void *callbackdata)
{
    ZIPinfo *info = ((ZIPinfo *) opaque);
    PHYSFS_sint32 dlen, dlen_inc;
    PHYSFS_sint64 max, i;

    i = zip_find_start_of_dir(info, dname, 0);
    if (i == -1)  /* no such directory. */
        return;

    dlen = strlen(dname);
    if ((dlen > 0) && (dname[dlen - 1] == '/')) /* ignore trailing slash. */
        dlen--;

    dlen_inc = ((dlen > 0) ? 1 : 0) + dlen;
    max = (PHYSFS_sint64) info->entryCount;
    while (i < max)
    {
        char *e = info->entries[i].name;
        if ((dlen) && ((strncmp(e, dname, dlen) != 0) || (e[dlen] != '/')))
            break;  /* past end of this dir; we're done. */

        if ((omitSymLinks) && (zip_entry_is_symlink(&info->entries[i])))
            i++;
        else
        {
            char *add = e + dlen_inc;
            char *ptr = strchr(add, '/');
            PHYSFS_sint32 ln = (PHYSFS_sint32) ((ptr) ? ptr-add : strlen(add));
            doEnumCallback(cb, callbackdata, origdir, add, ln);
            ln += dlen_inc;  /* point past entry to children... */

            /* increment counter and skip children of subdirs... */
            while ((++i < max) && (ptr != NULL))
            {
                char *e_new = info->entries[i].name;
                if ((strncmp(e, e_new, ln) != 0) || (e_new[ln] != '/'))
                    break;
            } /* while */
        } /* else */
    } /* while */
} /* ZIP_enumerateFiles */


static int ZIP_exists(dvoid *opaque, const char *name)
{
    int isDir;    
    ZIPinfo *info = (ZIPinfo *) opaque;
    ZIPentry *entry = zip_find_entry(info, name, &isDir);
    return((entry != NULL) || (isDir));
} /* ZIP_exists */


static PHYSFS_sint64 ZIP_getLastModTime(dvoid *opaque,
                                        const char *name,
                                        int *fileExists)
{
    int isDir;
    ZIPinfo *info = (ZIPinfo *) opaque;
    ZIPentry *entry = zip_find_entry(info, name, &isDir);

    *fileExists = ((isDir) || (entry != NULL));
    if (isDir)
        return(1);  /* Best I can do for a dir... */

    BAIL_IF_MACRO(entry == NULL, NULL, -1);
    return(entry->last_mod_time);
} /* ZIP_getLastModTime */


static int ZIP_isDirectory(dvoid *opaque, const char *name, int *fileExists)
{
    ZIPinfo *info = (ZIPinfo *) opaque;
    int isDir;
    ZIPentry *entry = zip_find_entry(info, name, &isDir);

    *fileExists = ((isDir) || (entry != NULL));
    if (isDir)
        return(1); /* definitely a dir. */

    /* Follow symlinks. This means we might need to resolve entries. */
    BAIL_IF_MACRO(entry == NULL, ERR_NO_SUCH_FILE, 0);

    if (entry->resolved == ZIP_UNRESOLVED_SYMLINK) /* gotta resolve it. */
    {
        int rc;
#if __MOJOSETUP__
        rc = zip_resolve(info->io, info, entry);
#else
        void *in = __PHYSFS_platformOpenRead(info->archiveName);
        BAIL_IF_MACRO(in == NULL, NULL, 0);
        rc = zip_resolve(in, info, entry);
        __PHYSFS_platformClose(in);
#endif
        if (!rc)
            return(0);
    } /* if */

    BAIL_IF_MACRO(entry->resolved == ZIP_BROKEN_SYMLINK, NULL, 0);
    BAIL_IF_MACRO(entry->symlink == NULL, ERR_NOT_A_DIR, 0);

    return(zip_find_start_of_dir(info, entry->symlink->name, 1) >= 0);
} /* ZIP_isDirectory */


static int ZIP_isSymLink(dvoid *opaque, const char *name, int *fileExists)
{
    int isDir;
    ZIPentry *entry = zip_find_entry((ZIPinfo *) opaque, name, &isDir);
    *fileExists = ((isDir) || (entry != NULL));
    BAIL_IF_MACRO(entry == NULL, NULL, 0);
    return(zip_entry_is_symlink(entry));
} /* ZIP_isSymLink */
#endif

static void *zip_get_file_handle(const char *fn, ZIPinfo *inf, ZIPentry *entry)
{
    int success;
#if __MOJOSETUP__
    MojoInput *io = (MojoInput *) inf->io;
    void *retval = io->duplicate(io);
#else
    void *retval = __PHYSFS_platformOpenRead(fn);
#endif
    BAIL_IF_MACRO(retval == NULL, NULL, NULL);

    success = zip_resolve(retval, inf, entry);
    if (success)
    {
        PHYSFS_sint64 offset;
        offset = ((entry->symlink) ? entry->symlink->offset : entry->offset);
        success = __PHYSFS_platformSeek(retval, offset);
    } /* if */

    if (!success)
    {
        __PHYSFS_platformClose(retval);
        retval = NULL;
    } /* if */

    return(retval);
} /* zip_get_file_handle */


static fvoid *ZIP_openRead(dvoid *opaque, const char *fnm, int *fileExists)
{
    ZIPinfo *info = (ZIPinfo *) opaque;
    ZIPentry *entry = zip_find_entry(info, fnm, NULL);
    ZIPfileinfo *finfo = NULL;
    void *in;

    *fileExists = (entry != NULL);
    BAIL_IF_MACRO(entry == NULL, NULL, NULL);

    in = zip_get_file_handle(info->archiveName, info, entry);
    BAIL_IF_MACRO(in == NULL, NULL, NULL);

    finfo = (ZIPfileinfo *) allocator.Malloc(sizeof (ZIPfileinfo));
    if (finfo == NULL)
    {
        __PHYSFS_platformClose(in);
        BAIL_MACRO(ERR_OUT_OF_MEMORY, NULL);
    } /* if */

    memset(finfo, '\0', sizeof (ZIPfileinfo));
    finfo->handle = in;
    finfo->entry = ((entry->symlink != NULL) ? entry->symlink : entry);
    initializeZStream(&finfo->stream);
    if (finfo->entry->compression_method != COMPMETH_NONE)
    {
        if (zlib_err(inflateInit2(&finfo->stream, -MAX_WBITS)) != Z_OK)
        {
            ZIP_fileClose(finfo);
            return(NULL);
        } /* if */

        finfo->buffer = (PHYSFS_uint8 *) allocator.Malloc(ZIP_READBUFSIZE);
        if (finfo->buffer == NULL)
        {
            ZIP_fileClose(finfo);
            BAIL_MACRO(ERR_OUT_OF_MEMORY, NULL);
        } /* if */
    } /* if */

    #if __MOJOSETUP__
    finfo->archive = info;
    #endif

    return(finfo);
} /* ZIP_openRead */


#if !__MOJOSETUP__
static fvoid *ZIP_openWrite(dvoid *opaque, const char *filename)
{
    BAIL_MACRO(ERR_NOT_SUPPORTED, NULL);
} /* ZIP_openWrite */


static fvoid *ZIP_openAppend(dvoid *opaque, const char *filename)
{
    BAIL_MACRO(ERR_NOT_SUPPORTED, NULL);
} /* ZIP_openAppend */
#endif


static void ZIP_dirClose(dvoid *opaque)
{
    ZIPinfo *zi = (ZIPinfo *) (opaque);
    zip_free_entries(zi->entries, zi->entryCount);
    allocator.Free(zi->archiveName);
    allocator.Free(zi);
} /* ZIP_dirClose */


#if !__MOJOSETUP__
static int ZIP_remove(dvoid *opaque, const char *name)
{
    BAIL_MACRO(ERR_NOT_SUPPORTED, 0);
} /* ZIP_remove */


static int ZIP_mkdir(dvoid *opaque, const char *name)
{
    BAIL_MACRO(ERR_NOT_SUPPORTED, 0);
} /* ZIP_mkdir */


const PHYSFS_ArchiveInfo __PHYSFS_ArchiveInfo_ZIP =
{
    "ZIP",
    ZIP_ARCHIVE_DESCRIPTION,
    "Ryan C. Gordon <icculus@icculus.org>",
    "http://icculus.org/physfs/",
};


const PHYSFS_Archiver __PHYSFS_Archiver_ZIP =
{
    &__PHYSFS_ArchiveInfo_ZIP,
    ZIP_isArchive,          /* isArchive() method      */
    ZIP_openArchive,        /* openArchive() method    */
    ZIP_enumerateFiles,     /* enumerateFiles() method */
    ZIP_exists,             /* exists() method         */
    ZIP_isDirectory,        /* isDirectory() method    */
    ZIP_isSymLink,          /* isSymLink() method      */
    ZIP_getLastModTime,     /* getLastModTime() method */
    ZIP_openRead,           /* openRead() method       */
    ZIP_openWrite,          /* openWrite() method      */
    ZIP_openAppend,         /* openAppend() method     */
    ZIP_remove,             /* remove() method         */
    ZIP_mkdir,              /* mkdir() method          */
    ZIP_dirClose,           /* dirClose() method       */
    ZIP_read,               /* read() method           */
    ZIP_write,              /* write() method          */
    ZIP_eof,                /* eof() method            */
    ZIP_tell,               /* tell() method           */
    ZIP_seek,               /* seek() method           */
    ZIP_fileLength,         /* fileLength() method     */
    ZIP_fileClose           /* fileClose() method      */
};

#else  // Glue from MojoSetup to PhysicsFS follows...


// MojoInput implementation...

static boolean MojoInput_zip_ready(MojoInput *io)
{
    return true;  // !!! FIXME: ready if there are bytes uncompressed.
} // MojoInput_zip_ready

static int64 MojoInput_zip_read(MojoInput *io, void *buf, uint32 bufsize)
{
    return ZIP_read(io->opaque, buf, 1, bufsize);
} // MojoInput_zip_read

static boolean MojoInput_zip_seek(MojoInput *io, uint64 pos)
{
    return ((ZIP_seek(io->opaque, pos)) ? true : false);
} // MojoInput_zip_seek

static int64 MojoInput_zip_tell(MojoInput *io)
{
    return ZIP_tell(io->opaque);
} // MojoInput_zip_tell

static int64 MojoInput_zip_length(MojoInput *io)
{
    return ZIP_fileLength(io->opaque);
} // MojoInput_zip_length

static MojoInput *buildZipMojoInput(ZIPinfo *info, const char *fullpath);

static MojoInput *MojoInput_zip_duplicate(MojoInput *io)
{
    ZIPfileinfo *finfo = (ZIPfileinfo *) io->opaque;
    return buildZipMojoInput(finfo->archive, finfo->entry->name);
} // MojoInput_zip_duplicate

static void MojoInput_zip_close(MojoInput *io)
{
    ZIP_fileClose(io->opaque);
    free(io);
} // MojoInput_zip_close


// MojoArchive implementation...

static int MojoArchive_zip_entry_is_symlink(ZIPinfo *info, ZIPentry *entry)
{
    if (entry->resolved == ZIP_UNRESOLVED_SYMLINK) /* gotta resolve it. */
        zip_resolve(info->io, info, entry);
    return zip_entry_is_symlink(entry);
} // MojoArchive_zip_entry_is_symlink


static boolean MojoArchive_zip_enumerate(MojoArchive *ar)
{
    ZIPinfo *info = (ZIPinfo *) ar->opaque;
    MojoArchive_resetEntry(&ar->prevEnum);
    info->enumIndex = 0;
    return true;
} // MojoArchive_zip_enumerate


static const MojoArchiveEntry *MojoArchive_zip_enumNext(MojoArchive *ar)
{
    ZIPinfo *info = (ZIPinfo *) ar->opaque;
    MojoArchiveEntry *retval = NULL;

    MojoArchive_resetEntry(&ar->prevEnum);

    if ((info->enumIndex >= 0) && (info->enumIndex < info->entryCount))
    {
        ZIPentry *entry = &info->entries[info->enumIndex];
        ar->prevEnum.filename = xstrdup(entry->name);
        ar->prevEnum.filesize = entry->uncompressed_size;
        ar->prevEnum.type = MOJOARCHIVE_ENTRY_FILE;
        ar->prevEnum.perms = entry->perms;

        if (entry->name[strlen(entry->name) - 1] == '/')
            ar->prevEnum.type = MOJOARCHIVE_ENTRY_DIR;
        else if (MojoArchive_zip_entry_is_symlink(info, entry))
        {
            ar->prevEnum.type = MOJOARCHIVE_ENTRY_SYMLINK;
            ar->prevEnum.linkdest = xstrdup(entry->linkdest);
        } // else if

        info->enumIndex++;
        retval = &ar->prevEnum;
    } // if

    return retval;
} // MojoArchive_zip_enumNext


static MojoInput *buildZipMojoInput(ZIPinfo *info, const char *fullpath)
{
    MojoInput *io = NULL;
    int exists = 0;
    void *opaque = ZIP_openRead(info, fullpath, &exists);
    if (opaque == NULL)
        return NULL;

    io = (MojoInput *) xmalloc(sizeof (MojoInput));
    io->ready = MojoInput_zip_ready;
    io->read = MojoInput_zip_read;
    io->seek = MojoInput_zip_seek;
    io->tell = MojoInput_zip_tell;
    io->length = MojoInput_zip_length;
    io->duplicate = MojoInput_zip_duplicate;
    io->close = MojoInput_zip_close;
    io->opaque = opaque;
    return io;
} // buildZipMojoInput


static MojoInput *MojoArchive_zip_openCurrentEntry(MojoArchive *ar)
{
    MojoInput *retval = NULL;
    ZIPinfo *info = (ZIPinfo *) ar->opaque;
    const int32 enumIndex = info->enumIndex - 1;

    if ((enumIndex >= 0) && (enumIndex < info->entryCount) &&
        (ar->prevEnum.type == MOJOARCHIVE_ENTRY_FILE))
    {
        char *fullpath = (char *) xmalloc(strlen(ar->prevEnum.filename) + 1);
        strcpy(fullpath, ar->prevEnum.filename);
        retval = buildZipMojoInput(info, fullpath);
        free(fullpath);
    } // if

    return retval;
} // MojoArchive_zip_openCurrentEntry


static void MojoArchive_zip_close(MojoArchive *ar)
{
    ZIP_dirClose(ar->opaque);
    ar->io->close(ar->io);
    MojoArchive_resetEntry(&ar->prevEnum);
    free(ar);
} // MojoArchive_zip_close


MojoArchive *MojoArchive_createZIP(MojoInput *io)
{
    MojoArchive *ar = NULL;
    void *opaque = ZIP_openArchive(io, "", 0);
    if (opaque == NULL)
        return NULL;

    ar = (MojoArchive *) xmalloc(sizeof (MojoArchive));
    ar->enumerate = MojoArchive_zip_enumerate;
    ar->enumNext = MojoArchive_zip_enumNext;
    ar->openCurrentEntry = MojoArchive_zip_openCurrentEntry;
    ar->close = MojoArchive_zip_close;
    ar->offsetOfStart = ((const ZIPinfo *) opaque)->offset;
    ar->opaque = opaque;
    ar->io = io;
    return ar;
} // MojoArchive_createZIP

#endif  // __MOJOSETUP__

#endif // SUPPORT_ZIP

// end of archive_zip.c ...

